-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Implement #:package
manipulation for file-based apps
#49635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR extends the dotnet package
commands to support file-based C# apps by introducing a --file
option, updating parsing/completion, and adding source‐editor logic to manipulate #:package
directives.
- Introduces
--file
alongside--project
in parsers and completion scripts - Implements add/remove of
#:package
directives inFileBasedAppSourceEditor
and wiring inPackageAddCommand
/PackageRemoveCommand
- Adds extensive unit tests and updates documentation for the new file-based scenarios
Reviewed Changes
Copilot reviewed 57 out of 57 changed files in this pull request and generated 2 comments.
Show a summary per file
File | Description |
---|---|
src/Cli/dotnet/Commands/Package/PackageCommandParser.cs | Adds FileOption and path‐processing logic for file-based apps |
src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs | Implements file‐based add logic, including CPM and restore support |
src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs | Implements file‐based remove logic and reports removed directives |
src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs | Refactors diagnostics handling and directive loading for files |
src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs | New editor for inserting/removing #:package directives |
src/Cli/dotnet/Commands/Run/RunCommand.cs | Updates error message constant for invalid option combinations |
Comments suppressed due to low confidence (1)
src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs:105
- Typo in variable name
addAfer
—it should be spelledaddAfter
to match the intended meaning.
CSharpDirective? addAfer = null;
@dotnet/run-file for reviews, thanks |
} | ||
} | ||
|
||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you can make arbitrary scopes like this, but what is this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows me to define the void Update(string value)
local function twice - otherwise the nested one would "see" the parent one and complain it's re-declared.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say that this is rather confusing: consider either using different names, such as UpdateInVersion
/UpdateInMetadata
, or combining the local function into one that takes an extra parameter, and then wrapping them in a lambda at the return to pass either versionAttribute
or metadata
.
@RikkiGibson @chsienki for reviews, thanks |
test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs
Outdated
Show resolved
Hide resolved
@RikkiGibson @jaredpar for another look, thanks |
@RikkiGibson @333fred for reviews, thanks |
} | ||
if (_arguments.Count != 1) | ||
|
||
var packageToRemove = arguments.Single(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can you just index? Or call First
? No need for assertions around the length, we just checked that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can use pattern matching. Thanks.
trailingNewLines = 0; | ||
} | ||
|
||
bool isEndOfLine = trivia.IsKind(SyntaxKind.EndOfLineTrivia); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure you're testing with a documentation comment at the top of the file, as well as things that will generate SkippedTokensTrivia. See https://github.com/dotnet/razor/blob/main/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/RoslynCSharpTokenizer.cs#L503 and surrounding cases for some examples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there tests for files with comments at the top?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, see test Comments
.
""" | ||
/* test */ | ||
|
||
#:package [email protected] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a bit subjective, but I'd rather see:
#:package MyPackage@1.0.0
/* test */ Console.WriteLine();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -18,7 +16,7 @@ public static class PackageAddCommandParser | |||
.AddCompletions((context) => | |||
{ | |||
// we should take --prerelease flags into account for version completion | |||
var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption); | |||
var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption!); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the order of the declarations is the only reason the suppressions are needed. That seems unfortunate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I can reorder, thanks.
if (!hasVersion) | ||
{ | ||
skipUpdate = true; | ||
return (Revert: NoOp, Update: Unreachable, Save: Revert); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am finding the flow here pretty wonky. It feels like some of the cases update the file on disk before we know whether that's what we should really do, and other cases wait to confirm that's what we should do before doing the update. Does it have to be that way?
I was also kinda hoping that the case where we need to update a Directory.Packages.props file could share more with the project-based version of the command. I guess that is not practical to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that once we have NuGet/Home#14390, lots of this logic can go away. But I didn't want to block the basic experience on that.
? (projectOrFile, AppKinds.Any) | ||
: (Environment.CurrentDirectory, AppKinds.ProjectBased), | ||
(true, false) => (parseResult.GetValue(FileOption)!, AppKinds.FileBased), | ||
(false, true) => (parseResult.GetValue(ProjectOption)!, AppKinds.ProjectBased), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So is the "optionality" of an Option<T>
indicated by giving it a nullable type argument? I am just curious if something would change for the worse here if FileOption
and ProjectOption
were Option<string>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wouldn't make a difference, the GetValue
returns nullable T
no matter what the original T
is:
public T? GetValue<T>(Option<T> option)
return new FileBasedAppSourceEditor | ||
{ | ||
SourceFile = sourceFile, | ||
Directives = LoadDirectives(sourceFile), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if Directives is just initialized based on value of SourceFile
, would it be cleaner to delete the setter for it and just rely on it being initialized on access?
|
||
public void Add(CSharpDirective directive) | ||
{ | ||
TextSpan span = DetermineWhereToAdd(directive, out var append); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Roslyn has a type TextChange
, which consists of a TextSpan
and string NewText
, and SourceText.WithChanges()
can take them to created edited versions of source files. Maybe it would be a bit cleaner to use that here?
|
||
// Find the last directive of the first group of directives of the same kind. | ||
// If found, we will insert the new directive after it. | ||
CSharpDirective? addAfer = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addAfter
?
// Otherwise, we will add the directive to the top of the file. | ||
int start = 0; | ||
|
||
var tokenizer = VirtualProjectBuildingCommand.CreateTokenizer(SourceFile.Text); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, this is an infrequent enough occurrence, that it would be fine to just parse, if it would make this implementation simpler. However, there's no strong need to make a change to do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that would be any simpler - we would get the same kind of result (syntax trivia) to work on, and parsing would just do more work than lexing does.
Part of #49200. This PR implements
dotnet package add
anddotnet package remove
.When more logic is shared with NuGet, more
package
-related commands can be implemented and some of the logic here simplified. See NuGet/Home#14390.I've modeled the commands around their project-based counterparts.
package remove
is relatively simple - corresponding#:package
directives are removed from the C# file,Directory.Packages.props
is not considered at all.package add
needs to updateDirectory.Packages.props
if Central Package Management is enabled. It also needs to perform restore. If no version is specified by the user on the command-line, it needs to determine the latest version and put that into the package reference directive.